package hw3.proxy.core;

import static hw3.proxy.core.ConfigurationProperties.MAX_RESPONSE_LEN_FOR_CACHING;
import static hw3.proxy.core.ConfigurationProperties.XHTML_MSG_PATH;
import static hw3.proxy.core.ProxyDBManager.ColumnNames;
import static hw3.proxy.core.ProxyDBManager.ETAG;
import static hw3.proxy.core.ProxyDBManager.EXPIRES;
import static hw3.proxy.core.ProxyDBManager.LASMODIFIED;
import static hw3.proxy.core.ProxyDBManager.RESPONSE;
import static hw3.proxy.core.ProxyDBManager.URL;
import hw3.exception.StdErrReporterException;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

import org.apache.http.Header;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultHttpRequestFactory;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.entity.EntityDeserializer;
import org.apache.http.impl.entity.StrictContentLengthStrategy;
import org.apache.http.impl.io.HttpResponseParser;
import org.apache.http.io.SessionInputBuffer;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicLineParser;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.util.EntityUtils;
import static org.apache.http.protocol.HTTP.*;

/**
 * This class is responsible for handling all aspects of HTTP message generation and parsing for the proxy server.
 * 
 * @author Adam Levi
 * @author Marina Skarbovsky
 *
 */
public class HttpMsgHandler
{
	/**
	 * Constructor
	 * 
	 * @param manager - a reference to the resource manager of the proxy server
	 */
	public HttpMsgHandler(ResourceManager manager)
	{
		m_ResourceManager = manager;
	}
	
	/**
	 * Handles the given HTTP request and generates an appropriate response (from the cache or from the origin server)
	 * @param requestToHandle - HTTP request
	 * @return HTTP response to the given request
	 * @throws IOException
	 * @throws HttpException
	 */
	public HttpResponse handleRequest(HttpRequestToHandle requestToHandle) throws IOException, HttpException
	{
		HttpResponse response = m_ResourceManager.getResource(requestToHandle);
		if (response == null)
		{
			response = create500Response(requestToHandle);
		}
		return response;
	}
	
	/**
	 * Clones an HTTP response
	 * @param response - clonee
	 * @return cloned request
	 * @throws HttpException
	 * @throws IOException
	 */
	public static HttpResponse cloneResponse(HttpResponse response) throws HttpException, IOException
	{
		HttpEntity entity = response.getEntity();
		StringEntity clonedEntity = null;
		if (entity != null)
		{
			String entityAsString = EntityUtils.toString(entity);
			String charset = EntityUtils.getContentCharSet(entity);
			/*if (charset == null)
			{
				charset = getCharSet(entityAsString);
			}*/
			clonedEntity = new StringEntity(entityAsString, charset);
		}
	
		HttpResponse clonedResponse = new BasicHttpResponse(response.getStatusLine());
		clonedResponse.setHeaders(response.getAllHeaders());	
		clonedResponse.setEntity(clonedEntity);
		return clonedResponse;	
	}
	
	/**
	 * Prints an HTTP response to the given stream
	 * @param response - HTTP response
	 * @param writer - Print Writer backed by a stream
	 * @throws IOException
	 * @throws HttpException
	 */
	public static void printResponse(HttpResponse response, PrintWriter writer) throws IOException, HttpException 
	{
		writer.println(response.getStatusLine().toString());
		for (HeaderIterator iter = response.headerIterator(); iter.hasNext(); ) 
		{
		    Header h = iter.nextHeader();
		    writer.println(h.toString());
		}
		writer.println();
		HttpEntity entity = response.getEntity();
		if (entity != null)
		{
			writer.println(EntityUtils.toString(entity, EntityUtils.getContentCharSet(response.getEntity())));
		}
	}
	
	/*static private String getCharSet(String body)
	{
		// get the Head part from body
		int beginIndex = body.indexOf("<head>");
		int endIndex = body.indexOf("</head>");
		if (beginIndex == -1 || endIndex == -1)
		{
			return null;
		}
		String headPart = body.substring(beginIndex + 6, endIndex);
		
		//String str = new String()
		
		// get the meta http-equiv="Content-Type" tag from the Head part
		beginIndex = headPart.indexOf(ContentTypeTag);
		if (beginIndex == -1)
		{
			return null;
		}
		
		endIndex = headPart.indexOf("/>", beginIndex + 6 + ContentTypeTag.length());
		if (endIndex == -1)
		{
			return null;
		}
		String contentTypeMetaTagValues = headPart.substring(beginIndex, endIndex);
		
		// find the content attribute
		
		beginIndex = contentTypeMetaTagValues.indexOf("charset=");
		if (beginIndex == -1)
		{
			return null;
		}
		
		endIndex = contentTypeMetaTagValues.indexOf("\"", beginIndex + 8);
		if (endIndex == -1)
		{
			return null;
		}
		String res = contentTypeMetaTagValues.substring(beginIndex + 8, endIndex);
		
		return res;
		
	}*/


	/**
	 * Attempts to extract the target's host name from the HTTP message by looking for a "Host" header or the URL
	 * @param msg - HTTP message
	 * @return the target's host name if found, or null otherwise.
	 */
	public static String getHostName(HttpMessage msg)
	{
		Header hostNameHeaderArray[] = msg.getHeaders("Host");
		String hostName = null;
		for (int i = 0; i < hostNameHeaderArray.length; i++)
		{
			if (hostNameHeaderArray[i].getName().equals("Host"))
			{
				hostName = hostNameHeaderArray[i].getValue();
				break;
			}
		}
		if (hostName == null) //no host header
		{
			//attempt to extract the host name from the URI in
			//the request
			if (msg.getClass().equals(HttpRequest.class))
			{
				HttpRequest request = (HttpRequest)msg;
				String uri = getRequestedURL(request);
				hostName = uri.substring(7).split("/")[0];
				hostName += "/";
			}
		}
		return hostName;
	}
	
	/**
	 * Checks whether the given HTTP response has a body
	 * @param response - HTTP response
	 * @return true if the given response has a body and false otherwise
	 */
	public static boolean isResponseEntityAvailable(HttpResponse response)
	{
		return (response.containsHeader(CONTENT_LEN) || response.containsHeader(TRANSFER_ENCODING));
	}

	/**
	 * Checks whether the given HTTP message contains a "Cache-Control" header with "no-cache" or "no-store" as values.
	 * @param message - HTTP message
	 * @return true if there's a "Cache-Control" header with one of the above mentioned values and false otherwise.
	 */
	public static boolean isNoCacheNoStore(HttpMessage message)
	{
		Header cacheControlHeaderArray[] = message.getHeaders("Cache-Control");
		boolean noCachenoStore = false;
		for (int i = 0; i < cacheControlHeaderArray.length; i++)
		{
			String currVal = cacheControlHeaderArray[i].getValue();
			if (currVal.contains("no-cache") || currVal.contains("no-store"))
			{
				noCachenoStore = true;
				break;
			}
		}
		return noCachenoStore;
	}
	
	/**
	 * Checks whether the given HTTP message contains at least one of the following freshness-control mechanism:
	 * <ol>
	 * <li>"Last-Modified" header</li>
	 * <li>"Expires" header</li>
	 * <li>"ETag" header</li>
	 * </ol>
	 * @param message - HTTP message
	 * @return true if some freshness-control mechanism exist and false otherwise
	 */
	public static boolean canBeValidated(HttpMessage message)
	{
		if (message.containsHeader("Last-Modified") ||
			message.containsHeader("Expires") ||
			message.containsHeader("ETag"))
		{
			return true;
		}
		return false;
	}

	/**
	 * Returns the requested URL in the given HTTP request
	 * @param request - HTTP request
	 * @return the requested URL 
	 */
	public static String getRequestedURL(HttpRequest request)
	{
		return request.getRequestLine().getUri();
	}

	/**
	 * Modifies a request received from a client to a request to be sent to the Origin server
	 * @param request - HTTP request as received from a client
	 * @return HTTP request that can be sent to an origin server to get the resource requested by the client
	 */
	public static HttpRequest createRequestForOriginSrv(HttpRequest request)
	{
		request.removeHeaders("Connection");
		request.removeHeaders("Keep-Alive");
		request.removeHeaders("If-Modified-Since");
		request.removeHeaders("If-None-Match");
		request.removeHeaders("Proxy-Connection");
		//we don't wish to receive zipped files from the origin server
		request.removeHeaders("Accept-Encoding");
		//notify the origin server to close the connection after sending the response
		request.addHeader("Connection", "close");
		//host header must be present in every valid HTTP 1.1 message
		if (!request.containsHeader("Host"))
		{
			request.addHeader("Host", getHostName(request));
		}
		return request;
	}

	/**
	 * Checks whether the given response should be cached or not. The following conditions are checked:
	 * <ol>
	 * <li>The status code is 200.</li>
	 * <li>The response is not marked as "no-cache" or "no-store".</li>
	 * <li>The response contains some sort of freshness-validation mechanism.</li>
	 * <li>The response is not larger than 64kb.</li>
	 * <li>The response is not encoded.</li>
	 * <li>The response is not chuncked.</li>
	 * </ol>
	 * @param response - HTTP response
	 * @return true if all of the above conditions hold (and the response is valid for caching) or false otherwise
	 * @throws ParseException
	 * @throws IOException
	 * @throws HttpException
	 */
	public static boolean isResponseSatisfyCacheCond(HttpResponse response) throws ParseException, IOException, HttpException
	{
		if (response.getStatusLine().getStatusCode() != 200)
		{
			return false;
		}
		else if (isNoCacheNoStore(response))
		{
			return false;
		}
		else if (!canBeValidated(response))
		{
			return false;
		}
		else if (expiresImmediately(response))
		{
			return false;
		}
		//responses larger than 64kb will not be cached
		else if (httpResponseToString(response).length() > MAX_RESPONSE_LEN_FOR_CACHING)
		{
			return false;
		}
		//encoded responses will not be cached
		else if (isEncoded(response))
		{
			return false;
		}
		//chuncked responses will not be cached
		else if (isChuncked(response))
		{
			return false;
		}
		return true;
	}

	private static boolean expiresImmediately(HttpResponse response)
	{
		if (!response.containsHeader("ETag") && 
			!response.containsHeader("Last-Modified") && 
			response.containsHeader("Expires") &&
			null == HttpDateParser.getExpirationTime(getExpiresValue(response), false))
		{
			return true;
		}
		return false;
	}

	/**
	 * Checks whether the message is encoded by checking the existence of 
	 * the "Content-Encoding" header.
	 * @param message - the HTTP message
	 * @return true if the message is encoded or false otherwise.
	 */
	private static boolean isEncoded(HttpMessage message)
	{
		return message.containsHeader("Content-Encoding");
	}
	
	/**
	 * Checks whether the message is chuncked by checking the existence of 
	 * the "Transfer-Encoding" header.
	 * @param message - the HTTP message
	 * @return true if the message is chuncked or false otherwise.
	 */
	private static boolean isChuncked(HttpMessage message)
	{
		return message.containsHeader(TRANSFER_ENCODING);
	}
	
	/**
	 * Checks whether token replacement is applicable for the supplied response. 
	 * @param response - HTTP response
	 * @return true if the token-replacement should be applied on the response, 
	 * i.e the response is not chuncked or encoded, or false otherwise.
	 */
	public static boolean isTokenReplacementAllowed(HttpResponse response)
	{
		if (isChuncked(response) || isEncoded(response))
		{
			return false;
		}
		return true;
	}
	/**
	 *returns the value of the first header with the given name
	 *
	 *@param message HTTP message the header appears in.
	 *@param headerName the name of header 
	 *
	 *@return the value of the first header in the message with the given name
	 */
	public static String getValueOfHeader(HttpMessage message, String headerName)
	{
		Header headers[] = message.getHeaders(headerName);
		if (headers.length > 0)
		{
			return headers[0].getValue();
		}
		return null;
	}
	
	/**
	 * returns the value of the "Last-Modified" header in the HTTP message as string
	 * @param message - HTTP message
	 * @return value of the "Last-Modified" header in the HTTP message as string
	 */
	public static String getLastModifiedValue(HttpMessage message)
	{
		return getValueOfHeader(message, "Last-Modified");
	}
	
	/**
	 * returns the value of the "Expires" header in the HTTP message as string
	 * @param message - HTTP message
	 * @return value of the "Expires" header in the HTTP message as string
	 */
	public static String getExpiresValue(HttpMessage message)
	{
		return getValueOfHeader(message, "Expires");
	}
	
	/**
	 * returns the value of the "ETag" header in the HTTP message as string
	 * @param message - HTTP message
	 * @return value of the "ETag" header in the HTTP message as string
	 */
	public static String getETagValue(HttpMessage message)
	{
		return getValueOfHeader(message, "ETag");
	}
	
	/**
	 * Creates an HTTP response from the given parameters
	 * @param hostName - target's host name
	 * @param statusCode - status code of the response
	 * @param body - response's body
	 * @return the generated HTTP response
	 */
	private static HttpResponse createBasicResponse(String hostName, int statusCode, String body)
	{
		DefaultHttpResponseFactory factory = new DefaultHttpResponseFactory();
		BasicHttpContext context = new BasicHttpContext();
		ProtocolVersion ver = new ProtocolVersion("HTTP", 1, 1);
		HttpResponse response = factory.newHttpResponse(ver, statusCode, context);
		response.setHeader("Host", hostName);
		response.setHeader("Connection", "close");
		if (body != null)
		{
			HttpEntity entity = new ByteArrayEntity(body.getBytes());
			response.setEntity(entity);
		}
		return response; 
	}
	
	/**
	 * Creates a 404 HTTP response 
	 * @param requestToHandle - the HTTP request that caused the generation of this response
	 * @return generated 404 response
	 */
	public static HttpResponse create404Response(HttpRequestToHandle requestToHandle)
	{
		 return createBasicResponse(requestToHandle.getClientHostName(), 404, proxyMessage404);
	 }
	 
	/**
	 * Creates a 500 HTTP response 
	 * @param requestToHandle - the HTTP request that caused the generation of this response
	 * @return generated 500 response
	 */
	 public static HttpResponse create500Response(HttpRequestToHandle requestToHandle)
	 {
		 return createBasicResponse(requestToHandle.getClientHostName(), 500, proxyMessage500);
	 }
	 
	/**
	 * Generates an HTTP request to be sent to an origin server in order to validate a resource, stored in the proxy, with the given URI.
	 * The validations is done using the last-modified and ETag fields of the resource, as stored in the proxy. 
	 * @param uri - resource's URI.
	 * @param lastModified - value of the "Last-Modified" header
	 * @param eTag - value of the "ETag" header
	 * @return the generated validation request. 
	 */
	public static HttpRequest createValidationRequest(String uri, String lastModified, String eTag) 
	{
		DefaultHttpRequestFactory factory = new DefaultHttpRequestFactory();
		HttpRequest request = null;
		//create a basic GET request
		try
		{
			request = factory.newHttpRequest("GET", uri);
		} 
		catch (MethodNotSupportedException e)
		{
			//this should never happen since the method "GET" is supported
			return null;
		}
		//set the host header
		request.setHeader("Host", uri.split("/")[2]);
		//set the validation headers according to the parameters
		if (lastModified != null)
		{
			request.setHeader("If-Modified-Since", lastModified);
		}
		if (eTag != null)
		{
			request.setHeader("If-None-Match", eTag);
		}
		request.setHeader("Connection", "close");
		return request; 
	}
	
	/**
	 * creates an array of values from the HTTP response
	 * <p>
	 * the returned array must be in the following form:
	 * <ol>
	 * <li>Result[0] = the URL </li>
	 * <li>Result[1] = the Response (Header + Body) </li>
	 * <li>Result[2] = the LastModified value (the original string) </li>
	 * <li>Result[3] = the Expires value (the original string) </li>
	 * <li>Result[4] = the ETag value (the original string) </li>
	 * </ol>
	 * if one of the values is missing from the Response then null should
	 * be placed instead (for instance: if there is no ETag in the response, null
	 * value will be placed in Result[4]) 
	 * </p>
	 * @param requested URL
	 * @param response An HTTP response
	 * @return an array of values that forms a cache entry
	 * @throws IOException 
	 * @throws ParseException 
	 * @throws HttpException 
	 */
	public static String[] createCacheEntry(String url, HttpResponse response) throws ParseException, IOException, HttpException
	{
		String cacheEntry[] = new String[ColumnNames.length];
		cacheEntry[URL] = url;
		cacheEntry[RESPONSE] = httpResponseToString(response);
		cacheEntry[LASMODIFIED] = getLastModifiedValue(response);
		cacheEntry[EXPIRES] = getExpiresValue(response);
		cacheEntry[ETAG] = getETagValue(response);
		return cacheEntry;
	}

	/**
	 * Return the given HTTP response as string
	 * @param response - HTTP response
	 * @return string representation of the response
	 * @throws ParseException
	 * @throws IOException
	 * @throws HttpException
	 */
	public static String httpResponseToString(HttpResponse response) throws ParseException, IOException, HttpException
	{
		StringWriter strWriter = new StringWriter();
		PrintWriter printWriter = new PrintWriter(strWriter, true);
		printResponse(response, printWriter);
		return strWriter.toString();
	
	}
	
	/**
	 * creates a response message from the given cached response
	 * @param cachedResponse A response string that is cached in the DB
	 * @return an object of type HttpResponse in case of success otherwise null.
	 * @throws HttpException 
	 * @throws IOException 
	 * @throws StdErrReporterException 
	 */
	public static HttpResponse createResponseFromString(String cachedResponse) throws IOException, HttpException
	{
		HttpEntity body = null;
		SessionInputBuffer inBuffer = new StringInputBuffer(cachedResponse);
		HttpResponseParser parser = new HttpResponseParser(
												inBuffer, 
												new BasicLineParser(), 
												new DefaultHttpResponseFactory(), 
												new BasicHttpParams());
		HttpResponse response = (HttpResponse)parser.parse();
		EntityDeserializer deserializer = new EntityDeserializer(new StrictContentLengthStrategy());
		body = deserializer.deserialize(inBuffer, response);
		response.setEntity(body);
		
		int indexBodyPartStart = cachedResponse.indexOf("\r\n\r\n");
		if (ConfigurationProperties.REPLACE_TOKENS && indexBodyPartStart != -1)
		{	
			String bodyAsString = cachedResponse.substring(indexBodyPartStart + 4);
			String charsetName = EntityUtils.getContentCharSet(body);
			int entityLen;
			if (charsetName != null)
			{
				entityLen = bodyAsString.getBytes(charsetName).length - 2;
			}
			else
			{
				entityLen = bodyAsString.getBytes().length - 2;
			}
			response.removeHeaders(CONTENT_LEN);
			response.addHeader(CONTENT_LEN, (new Integer(entityLen)).toString());
		}
		
		return response;
	}
	
	/**
	 * Checks whether the given HTTP response is textual. A response will be considered textual if one 
	 * of the following conditions hold:
	 * <ol>
	 * <li>The response contains a "Content-Type" header with one of the following values: text, javascript, xml or xhtml.</li>
	 * <li>The response contains a body of length 0.</li>
	 * @param response - HTTP response
	 * @return true if the response is textual or false otherwise.
	 */
	public static boolean isResponseTextual(HttpResponse response)
	{
		String contentTypeVal = getValueOfHeader(response, CONTENT_TYPE);
		if (contentTypeVal == null)
		{
			String contentLength = getValueOfHeader(response, CONTENT_LEN);
			if (contentLength != null && Long.parseLong(contentLength) == 0)
			{
				return true;
			}
			return false;
		}
		if (contentTypeVal.contains("text")  || contentTypeVal.contains("javascript") ||
			contentTypeVal.contains("xhtml") ||	contentTypeVal.contains("xml"))
		{
			return true;
		}
		return false;
	}
	
	/**
	 * Return the contents of the file with the given name as String
	 * @param fileName - path + file name
	 * @return the contents of the file as String
	 */
	public static String fileToString(String fileName)
	{
		File xhtmlFile = new File(fileName);
		BufferedInputStream bufferedInStream = null;
		byte buffer[] = null;
		try
		{
			bufferedInStream = new BufferedInputStream(new FileInputStream(xhtmlFile));
			buffer = new byte[(int)xhtmlFile.length()];
			bufferedInStream.read(buffer);
			return new String(buffer);
		} 
		catch (IOException e)
		{
			return null;
		}
		finally
		{
			try
			{
				bufferedInStream.close();
			} 
			catch (IOException e) {}
		}
	}
	
	private ResourceManager m_ResourceManager;
	private static String proxyMessage404; 
	private static String proxyMessage500;
	//private static final String ContentTypeTag = "<meta http-equiv=\"Content-Type\"";
	static
	{
		proxyMessage404 = fileToString(XHTML_MSG_PATH + "proxyMsg404.xhtml");
		proxyMessage500 = fileToString(XHTML_MSG_PATH + "proxyMsg500.xhtml");
	}
}
